/**
 * Copyright (c) 2009 Noel Ang
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
 * IN THE SOFTWARE.
 */
package com.lunarshades.samsara.logging;

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.TimeZone;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.event.UndoableEditEvent;
import javax.swing.event.UndoableEditListener;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import javax.swing.text.Element;
import javax.swing.text.PlainDocument;
import javax.swing.text.Position;
import javax.swing.text.Segment;

/**
 * A Logger implementation whose outlet is a {@link javax.swing.text.Document}.
 *
 * @author Noel Ang, sparta.variable@gmail.com
 */
public class SwingDocumentLogger extends Logger implements Document {

public SwingDocumentLogger() {
    super();
    mDocument = new PlainDocument();
    mBuffer = new StringBuffer();
    mDateTimeFormatter = new SimpleDateFormat("DDD:HH:mm");
    mLineCount = 0;
}

private void updateDocument() {
    synchronized (mDocument) {
        try {
            if (mLineCount == MAX_LINE_COUNT) {
                // Exceeding display capacity; remove oldest (top-most)
                // buffered line.
                Element root = mDocument.getDefaultRootElement();
                if (root.getElementCount() > 0) {
                    Element firstLine = root.getElement(0);
                    assert firstLine != null;
                    int length = firstLine.getEndOffset()
                            - firstLine.getStartOffset() + 1;
                    mDocument.remove(firstLine.getStartOffset(), length);
                }
            } else {
                mLineCount += 1;
            }

            // Add the new line
            mDocument.insertString(mDocument.getLength(),
                    mBuffer.toString(),
                    null
            );
        } catch (BadLocationException e) {
            e.printStackTrace();
            assert false;
            throw new RuntimeException(e);
        }
    }
}

/**
 * Consume the specified event.
 *
 * @param event A Loggable object.
 *
 * @throws IllegalStateException if {@link #dispose()} has been previously
 *                               invoked.
 */
public void log(Loggable event) {
    if (event == null) {
        return;
    }
    Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("GMT-0"));
    String time = mDateTimeFormatter.format(calendar.getTime());
    Object message = event.message();
    synchronized (mBuffer) {
        mBuffer.append(time).append(SPACE).append(message).append(NEWLINE);
        updateDocument();
        mBuffer.delete(0, mBuffer.length());
    }
}

/**
 * Decides if the specified event is fit for consumption.
 *
 * @param event The event whose suitability is to be determined.
 *
 * @return True if the specified event would cause a subsequent call to the
 *         implementation's {@link #log(Loggable)} to be meaningful; false
 *         otherwise.
 */
public boolean accept(Loggable event) {
    return event != null;
}

/**
 * Discard the logger instance. This method provides a chance for an
 * implementation to free up resources. Once this method is called the object
 * should no longer be used; its behaviour after that point is undefined.
 */
public void dispose() {
    // NO-OP
}

/**
 * Returns number of characters of content currently in the document.
 *
 * @return number of characters >= 0
 */
public int getLength() {
    synchronized (mDocument) {
        return mDocument.getLength();
    }
}

/**
 * Registers the given observer to begin receiving notifications when changes
 * are made to the document.
 *
 * @param listener the observer to register
 *
 * @see Document#removeDocumentListener
 */
public void addDocumentListener(DocumentListener listener) {
    synchronized (mDocument) {
        mDocument.addDocumentListener(listener);
    }
}

/**
 * Unregisters the given observer from the notification list so it will no
 * longer receive change updates.
 *
 * @param listener the observer to register
 *
 * @see Document#addDocumentListener
 */
public void removeDocumentListener(DocumentListener listener) {
    synchronized (mDocument) {
        mDocument.removeDocumentListener(listener);
    }
}

/**
 * Registers the given observer to begin receiving notifications when undoable
 * edits are made to the document.
 *
 * @param listener the observer to register
 *
 * @see UndoableEditEvent
 */
public void addUndoableEditListener(UndoableEditListener listener) {
    synchronized (mDocument) {
        mDocument.addUndoableEditListener(listener);
    }
}

/**
 * Unregisters the given observer from the notification list so it will no
 * longer receive updates.
 *
 * @param listener the observer to register
 *
 * @see UndoableEditEvent
 */
public void removeUndoableEditListener(UndoableEditListener listener) {
    synchronized (mDocument) {
        mDocument.removeUndoableEditListener(listener);
    }
}

/**
 * Gets the properties associated with the document.
 *
 * @param key a non-<code>null</code> property key
 *
 * @return the properties
 *
 * @see #putProperty(Object, Object)
 */
public Object getProperty(Object key) {
    synchronized (mDocument) {
        return mDocument.getProperty(key);
    }
}

/**
 * Associates a property with the document.  Two standard property keys
 * provided are: <a href="#StreamDescriptionProperty">
 * <code>StreamDescriptionProperty</code></a> and <a href="#TitleProperty"><code>TitleProperty</code></a>.
 * Other properties, such as author, may also be defined.
 *
 * @param key   the non-<code>null</code> property key
 * @param value the property value
 *
 * @see #getProperty(Object)
 */
public void putProperty(Object key, Object value) {
    synchronized (mDocument) {
        mDocument.putProperty(key, value);
    }
}

/**
 * Removes a portion of the content of the document. This will cause a
 * DocumentEvent of type DocumentEvent.EventType.REMOVE to be sent to the
 * registered DocumentListeners, unless an exception is thrown.  The
 * notification will be sent to the listeners by calling the removeUpdate
 * method on the DocumentListeners.
 * <p/>
 * To ensure reasonable behavior in the face of concurrency, the event is
 * dispatched after the mutation has occurred. This means that by the time a
 * notification of removal is dispatched, the document has already been updated
 * and any marks created by <code>createPosition</code> have already changed.
 * For a removal, the end of the removal range is collapsed down to the start
 * of the range, and any marks in the removal range are collapsed down to the
 * start of the range. <p align=center><img src="doc-files/Document-remove.gif"
 * alt="Diagram shows removal of 'quick' from 'The quick brown fox.'">
 * <p/>
 * If the Document structure changed as result of the removal, the details of
 * what Elements were inserted and removed in response to the change will also
 * be contained in the generated DocumentEvent. It is up to the implementation
 * of a Document to decide how the structure should change in response to a
 * remove.
 * <p/>
 * If the Document supports undo/redo, an UndoableEditEvent will also be
 * generated.
 *
 * @param offs the offset from the beginning >= 0
 * @param len  the number of characters to remove >= 0
 *
 * @throws BadLocationException some portion of the removal range was not a
 *                              valid part of the document.  The location in
 *                              the exception is the first bad position
 *                              encountered.
 * @see DocumentEvent
 * @see DocumentListener
 * @see UndoableEditEvent
 * @see UndoableEditListener
 */
public void remove(int offs, int len) throws BadLocationException {
    throw new BadLocationException("Read-only document", offs);
}

/**
 * Inserts a string of content.  This will cause a DocumentEvent of type
 * DocumentEvent.EventType.INSERT to be sent to the registered DocumentListers,
 * unless an exception is thrown. The DocumentEvent will be delivered by
 * calling the insertUpdate method on the DocumentListener. The offset and
 * length of the generated DocumentEvent will indicate what change was actually
 * made to the Document. <p align=center><img src="doc-files/Document-insert.gif"
 * alt="Diagram shows insertion of 'quick' in 'The quick brown fox'">
 * <p/>
 * If the Document structure changed as result of the insertion, the details of
 * what Elements were inserted and removed in response to the change will also
 * be contained in the generated DocumentEvent.  It is up to the implementation
 * of a Document to decide how the structure should change in response to an
 * insertion.
 * <p/>
 * If the Document supports undo/redo, an UndoableEditEvent will also be
 * generated.
 *
 * @param offset the offset into the document to insert the content >= 0. All
 *               positions that track change at or after the given location
 *               will move.
 * @param str    the string to insert
 * @param a      the attributes to associate with the inserted content.  This
 *               may be null if there are no attributes.
 *
 * @throws BadLocationException the given insert position is not a valid
 *                              position within the document
 * @see DocumentEvent
 * @see DocumentListener
 * @see UndoableEditEvent
 * @see UndoableEditListener
 */
public void insertString(int offset, String str, AttributeSet a)
        throws BadLocationException {
    throw new BadLocationException("Read-only document", offset);
}

/**
 * Fetches the text contained within the given portion of the document.
 *
 * @param offset the offset into the document representing the desired start of
 *               the text >= 0
 * @param length the length of the desired string >= 0
 *
 * @return the text, in a String of length >= 0
 *
 * @throws BadLocationException some portion of the given range was not a valid
 *                              part of the document.  The location in the
 *                              exception is the first bad position
 *                              encountered.
 */
public String getText(int offset, int length) throws BadLocationException {
    synchronized (mDocument) {
        return mDocument.getText(offset, length);
    }
}

/**
 * Fetches the text contained within the given portion of the document.
 * <p/>
 * If the partialReturn property on the txt parameter is false, the data
 * returned in the Segment will be the entire length requested and may or may
 * not be a copy depending upon how the data was stored. If the partialReturn
 * property is true, only the amount of text that can be returned without
 * creating a copy is returned.  Using partial returns will give better
 * performance for situations where large parts of the document are being
 * scanned.  The following is an example of using the partial return to access
 * the entire document:
 * <p/>
 * <pre><code>
 * <p/>
 * &nbsp; int nleft = doc.getDocumentLength();
 * &nbsp; Segment text = new Segment();
 * &nbsp; int offs = 0;
 * &nbsp; text.setPartialReturn(true);
 * &nbsp; while (nleft > 0) {
 * &nbsp;     doc.getText(offs, nleft, text);
 * &nbsp;     // do someting with text
 * &nbsp;     nleft -= text.count;
 * &nbsp;     offs += text.count;
 * &nbsp; }
 * <p/>
 * </code></pre>
 *
 * @param offset the offset into the document representing the desired start of
 *               the text >= 0
 * @param length the length of the desired string >= 0
 * @param txt    the Segment object to return the text in
 *
 * @throws BadLocationException Some portion of the given range was not a valid
 *                              part of the document.  The location in the
 *                              exception is the first bad position
 *                              encountered.
 */
public void getText(int offset, int length, Segment txt)
        throws BadLocationException {
    synchronized (mDocument) {
        mDocument.getText(offset, length, txt);
    }
}

/**
 * Returns a position that represents the start of the document.  The position
 * returned can be counted on to track change and stay located at the beginning
 * of the document.
 *
 * @return the position
 */
public Position getStartPosition() {
    synchronized (mDocument) {
        return mDocument.getStartPosition();
    }
}

/**
 * Returns a position that represents the end of the document.  The position
 * returned can be counted on to track change and stay located at the end of
 * the document.
 *
 * @return the position
 */
public Position getEndPosition() {
    synchronized (mDocument) {
        return mDocument.getEndPosition();
    }
}

/**
 * This method allows an application to mark a place in a sequence of character
 * content. This mark can then be used to tracks change as insertions and
 * removals are made in the content. The policy is that insertions always occur
 * prior to the current position (the most common case) unless the insertion
 * location is zero, in which case the insertion is forced to a position that
 * follows the original position.
 *
 * @param offs the offset from the start of the document >= 0
 *
 * @return the position
 *
 * @throws BadLocationException if the given position does not represent a
 *                              valid location in the associated document
 */
public Position createPosition(int offs) throws BadLocationException {
    throw new BadLocationException("Read-only document", offs);
}

/**
 * Returns all of the root elements that are defined. <p> Typically there will
 * be only one document structure, but the interface supports building an
 * arbitrary number of structural projections over the text data. The document
 * can have multiple root elements to support multiple document structures.
 * Some examples might be: </p> <ul> <li>Text direction. <li>Lexical token
 * streams. <li>Parse trees. <li>Conversions to formats other than the native
 * format. <li>Modification specifications. <li>Annotations. </ul>
 *
 * @return the root element
 */
public Element[] getRootElements() {
    synchronized (mDocument) {
        return mDocument.getRootElements();
    }
}

/**
 * Returns the root element that views should be based upon, unless some other
 * mechanism for assigning views to element structures is provided.
 *
 * @return the root element
 */
public Element getDefaultRootElement() {
    synchronized (mDocument) {
        return mDocument.getDefaultRootElement();
    }
}

/**
 * Allows the model to be safely rendered in the presence of concurrency, if
 * the model supports being updated asynchronously. The given runnable will be
 * executed in a way that allows it to safely read the model with no changes
 * while the runnable is being executed.  The runnable itself may <em>not</em>
 * make any mutations.
 *
 * @param r a <code>Runnable</code> used to render the model
 */
public void render(Runnable r) {
    synchronized (mDocument) {
        mDocument.render(r);
    }
}

private final static int MAX_LINE_COUNT = 100;

private final PlainDocument mDocument;
private final DateFormat mDateTimeFormatter;
private final StringBuffer mBuffer;
private volatile int mLineCount;
}
